package config

import (
	"encoding/json"
	"errors"
	"github.com/fsnotify/fsnotify"
	jsoniter "github.com/json-iterator/go"
	"github.com/shima-park/agollo"
	"github.com/spf13/cast"
	"os"
	"path/filepath"
	"runtime"
	"strings"
	"time"

	"github.com/sirupsen/logrus"
	"github.com/spf13/viper"
)

const (
	_defaultConfigName = "app"
)

var (
	AppId           = "10000"
	AppName         = "default"
	AppAddr         = "127.0.0.1:8080"
	AppPath         = ""
	Hostname        = "localhost"
	ServerName      = "default-server"
	AppReadTimeout  = 10
	AppWriteTimeout = 10

	LogLevel     = "info"
	LogFormatter = "text"
	LogTopic     = "golang_log"
	DbMode       = false

	// 临时目录路径
	TempDir = ""

	// skywalking服务器配置
	SkywalkingHost                 = ""
	SkywalkingSwitch               = false
	SkywalkingSamplingRate float64 = 0

	RunMode = "debug"

	// IsDevEnv 开发环境标志
	IsDevEnv = false
	// IsTestEnv 测试环境标志
	IsTestEnv = false
	// IsProdEnv 生产环境标志
	IsProdEnv = false
	// Env 运行环境
	Env = "dev"

	// 邮箱
	MailConfig = &Mailer{}

	// Redis
	Redis = &CacheRedis{}
	// RedisCluster
	RedisCluster = &CacheRedisCluster{}

	// apollo配置中心
	ApolloConfig agollo.Agollo
)

type Mailer struct {
	Host      string
	Port      int
	Username  string
	Password  string
	IsEnabled int
}

type Database struct {
	Type        string
	Dsn         string
	MaxIdle     int
	MaxActive   int
	IdleTimeout int
}

type Mongo struct {
	Dsn         string
	DialTimeout int64
}

type CacheRedis struct {
	MasterName    string
	SentinelAddrs []string
	Password      string
	DB            int
	ReadTimeout   time.Duration
	WriteTimeout  time.Duration
	IdleTimeout   time.Duration
}

// redist集群
type CacheRedisCluster struct {
	// 集群节点地址，理论上只要填一个可用的节点客户端就可以自动获取到集群的所有节点信息。但是最好多填一些节点以增加容灾能力，因为只填一个节点的话，如果这个节点出现了异常情况，则Go应用程序在启动过程中无法获取到集群信息。
	Addrs    []string `json:"addrs"`
	Password string   `json:"password"` // 密码

	// 只含读操作的命令的"节点选择策略"。默认都是false，即只能在主节点上执行。
	ReadOnly bool `json:"read_only"` // 置为true则允许在从节点上执行只含读操作的命令
	// 默认false。 置为true则ReadOnly自动置为true,表示在处理只读命令时，可以在一个slot对应的主节点和所有从节点中选取Ping()的响应时长最短的一个节点来读数据
	RouteByLatency bool `json:"route_by_latency"`
	// 默认false。置为true则ReadOnly自动置为true,表示在处理只读命令时，可以在一个slot对应的主节点和所有从节点中随机挑选一个节点来读数据
	RouteRandomly bool `json:"route_randomly"`
	//
	// // //用户可定制读取节点信息的函数，比如在非集群模式下可以从zookeeper读取。
	// // //但如果面向的是redis cluster集群，则客户端自动通过cluster slots命令从集群获取节点信息，不会用到这个函数。
	// ClusterSlots  func() ([]redis.ClusterSlot, error)
	//
	// // 钩子函数，当一个新节点创建时调用，传入的参数是新建的redis.Client
	// OnNewNode func(*redis.Client)
	//
	// // ------------------------------------------------------------------------------------------------------
	// // ClusterClient管理着一组redis.Client,下面的参数和非集群模式下的redis.Options参数一致，但默认值有差别。
	// // 初始化时，ClusterClient会把下列参数传递给每一个redis.Client
	//
	// // 钩子函数
	// // 仅当客户端执行命令需要从连接池获取连接时，如果连接池需要新建连接则会调用此钩子函数
	// OnConnect  func(conn *redis.Conn) error

	MaxRedirects int `json:"max_redirects"` // 当遇到网络错误或者MOVED/ASK重定向命令时，最多重试几次，默认8

	// 每一个redis.Client的连接池容量及闲置连接数量，而不是cluterClient总体的连接池大小。实际上没有总的连接池
	// 而是由各个redis.Client自行去实现和维护各自的连接池。
	PoolSize     int `json:"pool_size"`      // 连接池最大socket连接数，默认为5倍CPU数， 5 * runtime.NumCPU
	MinIdleConns int `json:"min_idle_conns"` // 在启动阶段创建指定数量的Idle连接，并长期维持idle状态的连接数不少于指定数量；。

	// 命令执行失败时的重试策略
	MaxRetries      int           `json:"max_retries"`       // 命令执行失败时，最多重试多少次，默认为0即不重试
	MinRetryBackoff time.Duration `json:"min_retry_backoff"` // 每次计算重试间隔时间的下限，默认8毫秒，-1表示取消间隔
	MaxRetryBackoff time.Duration `json:"max_retry_backoff"` // 每次计算重试间隔时间的上限，默认512毫秒，-1表示取消间隔

	// 超时
	DialTimeout  time.Duration `json:"dial_timeout"`  // 连接建立超时时间，默认5秒。
	ReadTimeout  time.Duration `json:"read_timeout"`  // 读超时，默认3秒， -1表示取消读超时
	WriteTimeout time.Duration `json:"write_timeout"` // 写超时，默认等于读超时，-1表示取消读超时
	PoolTimeout  time.Duration `json:"pool_timeout"`  // 当所有连接都处在繁忙状态时，客户端等待可用连接的最大等待时长，默认为读超时+1秒。

	// 闲置连接检查包括IdleTimeout，MaxConnAge
	IdleCheckFrequency time.Duration `json:"idle_check_frequency"` // 闲置连接检查的周期，无默认值，由ClusterClient统一对所管理的redis.Client进行闲置连接检查。初始化时传递-1给redis.Client表示redis.Client自己不用做周期性检查，只在客户端获取连接时对闲置连接进行处理。
	IdleTimeout        time.Duration `json:"idle_timeout"`         // 闲置超时，默认5分钟，-1表示取消闲置超时检查
	MaxConnAge         time.Duration `json:"max_conn_age"`         // 连接存活时长，从创建开始计时，超过指定时长则关闭连接，默认为0，即不关闭存活时长较长的连接
}

func init() {
	Hostname, _ = os.Hostname()
	Env = os.Getenv("GO_RUNMODE")

	switch Env {
	case "dev":
		IsDevEnv = true
		RunMode = "debug"
	case "test":
		IsTestEnv = true
		RunMode = "test"
	default:
		IsProdEnv = true
		RunMode = "release"
	}

	configName := _defaultConfigName
	if Env != "" {
		configName = configName + "." + Env
	}

	configPath := os.Getenv("CONF_PATH")
	if configPath == "" {
		logger().Warn("env CONF_PATH is empty")

		var err error
		if AppPath, err = filepath.Abs(filepath.Dir(os.Args[0])); err != nil {
			panic(err)
		}

		workPath, err := os.Getwd()
		if err != nil {
			panic(err)
		}

		configPath = filepath.Join(workPath, "conf") // 先找当前目录conf下面的文件
		configName, err = findConfName(configPath, configName)
		if err != nil {
			configPath = filepath.Join(AppPath, "conf") // 查找app路径conf下面的文件
			configName, err = findConfName(configPath, configName)
			if err != nil {
				logger().WithField("path", configPath).Info("not found")
			}
		}
	}

	viper.SetConfigName(configName)

	viper.AddConfigPath(configPath)

	if err := viper.ReadInConfig(); err != nil {
		logger().Error(err)
	}

	viper.AutomaticEnv()

	// 初始化apollo配置中心
	ApolloConfig = InitApolloConfig()

	// 初始化其它配置
	NewAppConfig()

	Redis = NewRedis("")
	// redis集群
	RedisCluster = NewRedisCluster("")
}

// 查找配置文件路径
func findConfName(path, name string) (string, error) {
	fullPath := path + "/" + name + ".toml"

	if fileExists(fullPath) {
		logger().WithField("path", fullPath).Info("use conf path")
		return name, nil
	}

	if name != _defaultConfigName { // 查找默认配置文件
		fullPath = path + "/" + _defaultConfigName + ".toml"
		if fileExists(fullPath) {
			logger().WithField("path", fullPath).Info("use conf path")
			return _defaultConfigName, nil
		}
	}

	return name, errors.New("not found")
}

func fileExists(name string) bool {
	if _, err := os.Stat(name); err != nil {
		if os.IsNotExist(err) {
			return false
		}
	}
	return true
}

func NewAppConfig() {
	AppId = GetString("app.AppId")
	AppName = GetString("app.AppName")

	appAddr := GetString("app.AppAddr")
	if appAddr != "" {
		AppAddr = appAddr
	}

	port := os.Getenv("PORT") // 优先读取环境变量的PORT
	if port != "" {
		AppAddr = ":" + port
	}

	appReadTimeout := GetInt("app.ReadTimeout")
	if appReadTimeout != 0 {
		AppReadTimeout = appReadTimeout
	}

	appWriteTimeout := GetInt("app.WriteTimeout")
	if appWriteTimeout != 0 {
		AppWriteTimeout = appWriteTimeout
	}

	logLevel := GetString("log.LogLevel")
	if logLevel != "" {
		LogLevel = logLevel
	}

	// 临时目录配置
	td := ""
	if runtime.GOOS == "windows" {
		td = GetString("dirPath.TempDirWindows")
	} else {
		td = GetString("dirPath.TempDir")
	}
	if td != "" {
		_, err := os.Stat(td)
		if err != nil {
			_ = os.MkdirAll(td, 777)
		}
		TempDir = td
	}

	// skywalking服务器配置
	SkywalkingSwitch = GetBool("skywalking.SwitchOn")
	SkywalkingHost = GetString("skywalking.Host")
	SkywalkingSamplingRate = GetFloat64("skywalking.SamplingRate")

	// 邮箱发送配置
	MailConfig.Host = GetString("mail.Host")
	MailConfig.Port = GetInt("mail.Port")
	MailConfig.Username = GetString("mail.Username")
	MailConfig.Password = GetString("mail.Password")
	MailConfig.IsEnabled = GetInt("mail.IsEnabled")

	logFormatter := GetString("log.LogFormatter")
	if logFormatter != "" {
		LogFormatter = logFormatter
	}

	logTopic := GetString("log.LogTopic")
	if logTopic != "" {
		LogTopic = LogTopic + "_" + logTopic
	}

	DbMode = GetBool("log.DbMode")
}

func NewDbConfig(key string) *Database {
	maxIdle := GetInt("db_" + key + ".maxidle")
	maxActive := GetInt("db_" + key + ".maxactive")
	idleTimeout := GetInt("db_" + key + ".idletimeout")
	dbType := GetString("db_" + key + ".type")
	dsn := GetString("db_" + key + ".dsn")

	dbConfig := &Database{
		Type:        dbType,
		Dsn:         dsn,
		MaxIdle:     maxIdle,
		MaxActive:   maxActive,
		IdleTimeout: idleTimeout,
	}
	return dbConfig
}

func NewMongoConfig(key string) *Mongo {
	dsn := GetString("mgo_" + key + ".dsn")
	dialTimeout := GetInt64("mgo_" + key + ".dialtimeout")

	mgoConfig := &Mongo{
		Dsn:         dsn,
		DialTimeout: dialTimeout,
	}

	return mgoConfig
}

func NewRedis(key string) *CacheRedis {
	redisKey := "redis"
	if key != "" {
		redisKey = redisKey + "_" + key
	}

	masterName := GetString(redisKey + ".mastername")
	sentinelAddrs := GetStringSlice(redisKey + ".sentineladdrs")
	sentinelPassword := GetString(redisKey + ".sentinelpassword")
	dbNum := GetInt(redisKey + ".dbnum")
	readTimeout := GetInt(redisKey + ".readtimeout")
	writeTimeout := GetInt(redisKey + ".writetimeout")
	idleTimeout := GetInt(redisKey + ".idletimeout")

	redisConfig := &CacheRedis{
		MasterName:    masterName,
		SentinelAddrs: sentinelAddrs,
		Password:      sentinelPassword,
		DB:            dbNum,
		ReadTimeout:   time.Duration(readTimeout) * time.Second,
		WriteTimeout:  time.Duration(writeTimeout) * time.Second,
		IdleTimeout:   time.Duration(idleTimeout) * time.Second,
	}

	return redisConfig
}

// Redis集群配置
func NewRedisCluster(key string) *CacheRedisCluster {
	redisKey := "redis_cluster"
	if key != "" {
		redisKey = redisKey + "_" + key
	}

	redisConfig := &CacheRedisCluster{
		MaxRedirects: 8, // 当遇到网络错误或者MOVED/ASK重定向命令时，最多重试几次，默认8
		// 只含读操作的命令的"节点选择策略"。默认都是false，即只能在主节点上执行。
		ReadOnly: false, // 置为true则允许在从节点上执行只含读操作的命令
		// 默认false。 置为true则ReadOnly自动置为true,表示在处理只读命令时，可以在一个slot对应的主节点和所有从节点中选取Ping()的响应时长最短的一个节点来读数据
		RouteByLatency: false,
		// 默认false。置为true则ReadOnly自动置为true,表示在处理只读命令时，可以在一个slot对应的主节点和所有从节点中随机挑选一个节点来读数据
		RouteRandomly: false,

		// 每一个redis.Client的连接池容量及闲置连接数量，而不是cluterClient总体的连接池大小。实际上没有总的连接池
		// 而是由各个redis.Client自行去实现和维护各自的连接池。
		PoolSize:     5 * runtime.NumCPU(), // 连接池最大socket连接数，默认为5倍CPU数， 5 * runtime.NumCPU
		MinIdleConns: 10,                   // 在启动阶段创建指定数量的Idle连接，并长期维持idle状态的连接数不少于指定数量；。

		// 命令执行失败时的重试策略
		MaxRetries:      0,                      // 命令执行失败时，最多重试多少次，默认为0即不重试
		MinRetryBackoff: 8 * time.Millisecond,   // 每次计算重试间隔时间的下限，默认8毫秒，-1表示取消间隔
		MaxRetryBackoff: 512 * time.Millisecond, // 每次计算重试间隔时间的上限，默认512毫秒，-1表示取消间隔

		// 超时
		DialTimeout:  5 * time.Second, // 连接建立超时时间，默认5秒。
		ReadTimeout:  3 * time.Second, // 读超时，默认3秒， -1表示取消读超时
		WriteTimeout: 3 * time.Second, // 写超时，默认等于读超时，-1表示取消读超时
		PoolTimeout:  4 * time.Second, // 当所有连接都处在繁忙状态时，客户端等待可用连接的最大等待时长，默认为读超时+1秒。

		// 闲置连接检查包括IdleTimeout，MaxConnAge
		IdleCheckFrequency: 60 * time.Second, // 闲置连接检查的周期，无默认值，由ClusterClient统一对所管理的redis.Client进行闲置连接检查。初始化时传递-1给redis.Client表示redis.Client自己不用做周期性检查，只在客户端获取连接时对闲置连接进行处理。
		IdleTimeout:        5 * time.Minute,  // 闲置超时，默认5分钟，-1表示取消闲置超时检查
		MaxConnAge:         0 * time.Second,  // 连接存活时长，从创建开始计时，超过指定时长则关闭连接，默认为0，即不关闭存活时长较长的连接
	}

	// 集群节点地址，理论上只要填一个可用的节点客户端就可以自动获取到集群的所有节点信息。但是最好多填一些节点以增加容灾能力，因为只填一个节点的话，如果这个节点出现了异常情况，则Go应用程序在启动过程中无法获取到集群信息。
	redisConfig.Addrs = GetStringSlice(redisKey + ".addrs")
	redisConfig.Password = GetString(redisKey + ".password") // 密码

	// 处理集群地址和密码外，其他配置项
	conf := map[string]interface{}{}
	cfgs := []map[string]string{
		{
			"keys": "read_only,route_by_latency,route_randomly",
			"type": "bool",
		}, {
			"keys": "max_redirects,pool_size,min_idle_conns,max_retries,min_retry_backoff",
			"type": "int",
		},
		{
			"keys": "max_retry_backoff,dial_timeout,read_timeout,write_timeout,pool_timeout,idle_check_frequency,idle_timeout,max_conn_age",
			"type": "time.Duration",
		},
	}
	// apollo 配置了集群,
	if len(redisConfig.Addrs) > 0 {
		for _, v := range cfgs {
			for _, key := range strings.Split(v["keys"], ",") {
				// apollo配置了该key
				akey := redisKey + "." + key
				if IsSet(akey) {
					switch v["type"] {
					case "bool":
						conf[key] = GetBool(akey)
					case "int":
						conf[key] = GetInt(akey)
					case "time.Duration":
						conf[key] = GetDuration(akey)
					}
				}
			}
		}
	} else {
		// 未配置apollo ，读取app.toml的配置
		confToml := viper.GetStringMapString(redisKey)
		// app.toml也没配置，返回
		if len(confToml) == 0 {
			return redisConfig
		}
		redisConfig.Addrs = strings.Split(cast.ToString(confToml["addrs"]), ",")
		redisConfig.Password = cast.ToString(confToml["password"])

		for _, v := range cfgs {
			for _, key := range strings.Split(v["keys"], ",") {
				// toml配置了key
				if cv, ok := confToml[key]; ok {
					switch v["type"] {
					case "bool":
						conf[key] = cast.ToBool(cv)
					case "int":
						conf[key] = cast.ToInt(cv)
					case "time.Duration":
						conf[key] = cast.ToInt64(cv)
					}
				}
			}
		}
	}
	// 配置了其他选项
	if len(conf) > 0 {
		str, _ := jsoniter.MarshalToString(conf)
		// 补充其他配置
		jsoniter.UnmarshalFromString(str, redisConfig)
	}

	return redisConfig
}

// apollo config 获取配置
func ApolloGet(key string) string {
	// 配置key统一转为小写读取、大小写敏感
	key = strings.ToLower(key)
	configValue := ""

	// 未配置apollo,读取app.toml
	if ApolloConfig == nil {
		return viper.GetString(key)
	}

	// 获取所有命名空间
	namespaceNameSlice := getApolloNamespace()
	for _, namespaceName := range namespaceNameSlice {
		// 获取指定命名空间的配置值
		configValue := ApolloConfig.Get(key, agollo.WithNamespace(namespaceName))
		if len(configValue) > 0 {
			return configValue
		}
	}
	return configValue
}

// GetFloat64 获取浮点数配置
func GetFloat64(key string) float64 {
	return cast.ToFloat64(ApolloGet(key))
}

// GetString 获取字符串配置
func GetString(key string) string {
	return ApolloGet(key)
}

// GetInt 获取整数配置
func GetInt(key string) int {
	return cast.ToInt(ApolloGet(key))
}

// GetInt32 获取 int32 配置
func GetInt32(key string) int32 {
	return cast.ToInt32(ApolloGet(key))
}

// GetInt64 获取 int64 配置
func GetInt64(key string) int64 {
	return cast.ToInt64(ApolloGet(key))
}

// GetDuration 获取时间配置
func GetDuration(key string) time.Duration {
	return cast.ToDuration(ApolloGet(key))
}

// GetBool 获取配置布尔配置
func GetBool(key string) bool {
	return cast.ToBool(ApolloGet(key))
}

// GetStringSlice 获取配置字符串切片配置
func GetStringSlice(key string) []string {
	var value []string
	strs := ApolloGet(key)
	// 未填写，直接返回
	if len(strs) == 0 {
		return value
	}
	// 解析json失败， apollo配置的是json切片， app.toml配置的是逗号连接的字符串
	if err := json.Unmarshal([]byte(strs), &value); err != nil {
		value = strings.Split(strs, ",")
	}
	return value
}

// GetStringMap 获取配置map配置
func GetStringMap(key string) map[string]interface{} {
	var value map[string]interface{}
	_ = json.Unmarshal([]byte(ApolloGet(key)), &value)
	return value
}

// GetStringMapString 获取配置字符串map配置
func GetStringMapString(key string) map[string]string {
	value := GetStringMap(key)
	return cast.ToStringMapString(value)
}

// IsSet 配置是否存在
func IsSet(key string) bool {
	value := ApolloGet(key)
	if len(value) <= 0 {
		return false
	}
	return true
}

// Set 设置配置，仅用于测试
func Set(key string, value string) {
	viper.Set(key, value)
}

// OnConfigChange 注册配置文件变更回调
// 需要在 WatchConfig 之前调用
func OnConfigChange(run func()) {
	viper.OnConfigChange(func(in fsnotify.Event) { run() })
}

// WatchConfig 启动配置变更监听，业务代码不要调用。
func WatchConfig() {
	viper.WatchConfig()
}

var levels = map[string]logrus.Level{
	"panic": logrus.PanicLevel,
	"fatal": logrus.FatalLevel,
	"error": logrus.ErrorLevel,
	"warn":  logrus.WarnLevel,
	"info":  logrus.InfoLevel,
	"debug": logrus.DebugLevel,
}

func logger() *logrus.Entry {
	if level, ok := levels[os.Getenv("LOG_LEVEL")]; ok {
		logrus.SetLevel(level)
	} else {
		logrus.SetLevel(logrus.DebugLevel)
	}

	return logrus.WithFields(logrus.Fields{
		"app_id":      AppId,
		"instance_id": Hostname,
		"env":         Env,
	})
}
